/* * Copyright 2016 Google Inc. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.samples.apps.iosched.testutils; import android.net.Uri; import android.support.test.espresso.UiController; import android.support.test.espresso.ViewAction; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import com.google.samples.apps.iosched.util.TimeUtils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.hamcrest.Matcher; import java.util.List; import static android.support.test.espresso.Espresso.onView; import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; /** * Provides helper method to get the text from a matched view. */ public class MatchersHelper { /** * This returns the text for the view that has been matched with {@code matcher}. The matched * view is expected to be a {@link TextView}. This is different from matching a view by text, * this is intended to be used when matching a view by another mean (for example, by id) but * when we need to know the text of that view, for use later in a test. * <p/> * In general, this isn't good practice as tests should be written using mock data (so we always * know what the data is) but this enables us to write tests that are written using real data * (so we don't always know what the data is but we want to verify something later in a test). * This is enables us to write UI tests before refactoring a feature to make it easier to mock * the data. */ public static String getText(final Matcher<View> matcher) { /** * We cannot use a String directly as we need to make it final to access it inside the * inner method but we cannot reassign a value to a final String. */ final String[] stringHolder = {null}; onView(matcher).perform(new ViewAction() { @Override public Matcher<View> getConstraints() { return isAssignableFrom(TextView.class); } @Override public String getDescription() { return "getting text from a TextView"; } @Override public void perform(UiController uiController, View view) { TextView tv = (TextView) view; stringHolder[0] = tv.getText().toString(); } }); return stringHolder[0]; } /** * This returns the text for the view that has an id of {@code idOfDescendantViewToGetTextFor} * and is a descendant of the nth child of {@code parentViewWithMultipleChildren}, where n * equals {@code indexOfDescendant}. The parent view is expected to be a {@link ViewGroup} and * the descendant view is expected to be a {@link TextView}. This is used when there is a * certain randomness in the elements shown in a collection view, even with mock data. * * @see com.google.samples.apps.iosched.videolibrary.VideoLibraryModel */ public static String getTextForViewGroupDescendant( final Matcher<View> parentViewWithMultipleChildren, final int indexOfDescendant, final int idOfDescendantViewToGetTextFor) { /** * We cannot use a String directly as we need to make it final to access it inside the * inner method but we cannot reassign a value to a final String. */ final String[] stringHolder = {null}; onView(parentViewWithMultipleChildren).perform(new ViewAction() { @Override public Matcher<View> getConstraints() { return isAssignableFrom(ViewGroup.class); } @Override public String getDescription() { return "getting text from a TextView with a known id and that is a descendant of " + "the nth child of a viewgroup"; } @Override public void perform(UiController uiController, View view) { ViewGroup vg = (ViewGroup) view; View nthDescendant = vg.getChildAt(indexOfDescendant); TextView matchedView = (TextView) nthDescendant.findViewById(idOfDescendantViewToGetTextFor); stringHolder[0] = matchedView.getText().toString(); } }); return stringHolder[0]; } /** * This returns the number of descendants of the {@code parentViewWithMultipleChildren} that * have an id of {@code idOfDescendantViewToGetTextFor}}. The parent view is expected to be a * {@link ViewGroup}. This is used when there is a certain randomness in the elements shown in a * collection view, even with mock data, but we want to check the number of elements inside the * parent view. * * @see com.google.samples.apps.iosched.explore.ExploreIOActivityTest */ public static int getNumberOfDescendantsForViewGroupDescendant( final Matcher<View> parentViewWithMultipleChildren, final int idOfDescendantViewToGetTextFor) { /** * We cannot use a int directly as we need to make it final to access it inside the * inner method but we cannot reassign a value to a final int. */ final int[] intHolder = {0}; onView(parentViewWithMultipleChildren).perform(new ViewAction() { @Override public Matcher<View> getConstraints() { return isAssignableFrom(ViewGroup.class); } @Override public String getDescription() { return "getting text from a TextView with a known id and that is a descendant of " + "the nth child of a viewgroup"; } @Override public void perform(UiController uiController, View view) { ViewGroup vg = (ViewGroup) view; int descendants = vg.getChildCount(); for (int i = 0; i < descendants; i++) { View ithDescendant = vg.getChildAt(i); View matchedView = ithDescendant.findViewById(idOfDescendantViewToGetTextFor); if (matchedView != null) { intHolder[0] += 1; } } } }); return intHolder[0]; } /** * Some {@link Uri} include specific times, for example all sessions after a given time. Even * though time can be set in tests, {@link com.google.samples.apps.iosched.util.TimeUtils * .getCurrentTime()} takes into account the passage of time so two separate calls to it will * return slightly different values. This matcher applies an approximation of 10 seconds. * <p/> * Note: it assumes the time portion of the {@link Uri} is the last segment. */ public static Matcher<Uri> approximateTimeUriMatcher(final Uri expectedUri) { return new BaseMatcher<Uri>() { @Override public void describeTo(final Description description) { description.appendText("approximateTimeUriMatcher"); } @Override public boolean matches(final Object item) { if (item instanceof Uri) { Uri actualUri = (Uri) item; // Check the last segment is a time long expectedTime = 0L; long actualTime = 0L; try { expectedTime = Long.parseLong(expectedUri.getLastPathSegment()); } catch (NumberFormatException e) { return false; } try { actualTime = Long.parseLong(actualUri.getLastPathSegment()); } catch (NumberFormatException e) { return false; } // If the times are within 10 seconds of each other, check other segments if (Math.abs(actualTime - expectedTime) < TimeUtils.SECOND * 10) { List<String> actualSegments = actualUri.getPathSegments(); List<String> expectedSegments = expectedUri.getPathSegments(); // If same number of segments if (actualSegments.size() == expectedSegments.size()) { boolean diffFound = false; // Check each segment except the last for (int i = 0; i < actualSegments.size() - 1; i++) { if (!actualSegments.get(i).equals(expectedSegments.get(i))) { diffFound = true; } } // They match only if no differences were found return !diffFound; } } } return false; } @Override public void describeMismatch(final Object item, final Description mismatchDescription) { mismatchDescription.appendText("item doesn't match uri " + expectedUri + " with an approximataion of 10 seconds for the last time segment"); } }; } }